use loco_rs::prelude::*;
use sea_orm::DeleteResult;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

pub use super::_entities::personal_access_tokens::{self, ActiveModel, Entity, Model};

#[derive(Debug, Deserialize, Serialize)]
pub struct CreateParams {
    pub tokenable_type: String,
    pub tokenable_id: String,
    pub name: String,
    pub token: String,
    pub abilities: Option<String>,
    pub expires_at: Option<DateTimeWithTimeZone>,
}

#[derive(Debug, Validate, Deserialize)]
pub struct Validator {
    #[validate(length(min = 1, message = "Tokenable type must not be empty."))]
    pub tokenable_type: String,
    #[validate(length(min = 1, message = "Tokenable ID must not be empty."))]
    pub tokenable_id: String,
    #[validate(length(min = 1, message = "Name must not be empty."))]
    pub name: String,
    #[validate(length(min = 1, message = "Token must not be empty."))]
    pub token: String,
}

impl Validatable for ActiveModel {
    fn validator(&self) -> Box<dyn Validate> {
        Box::new(Validator {
            tokenable_type: self.tokenable_type.as_ref().to_owned(),
            tokenable_id: self.tokenable_id.as_ref().to_owned(),
            name: self.name.as_ref().to_owned(),
            token: self.token.as_ref().to_owned(),
        })
    }
}

#[async_trait::async_trait]
impl ActiveModelBehavior for super::_entities::personal_access_tokens::ActiveModel {
    async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
    where
        C: ConnectionTrait,
    {
        self.validate()?;
        if insert {
            let mut this = self;
            this.id = ActiveValue::Set(Uuid::new_v4().to_string());
            Ok(this)
        } else {
            Ok(self)
        }
    }
}

impl Model {
    /// finds a personal access token by the provided id
    ///
    /// # Errors
    ///
    /// When could not find personal access token or DB query error
    pub async fn find_by_id(db: &DatabaseConnection, id: &str) -> ModelResult<Self> {
        let token = personal_access_tokens::Entity::find()
            .filter(
                model::query::condition()
                    .eq(personal_access_tokens::Column::Id, id)
                    .build(),
            )
            .one(db)
            .await?;
        token.ok_or_else(|| ModelError::EntityNotFound)
    }

    /// finds a personal access token by the provided token string
    ///
    /// # Errors
    ///
    /// When could not find personal access token or DB query error
    pub async fn find_by_token(db: &DatabaseConnection, token: &str) -> ModelResult<Self> {
        let token = personal_access_tokens::Entity::find()
            .filter(
                model::query::condition()
                    .eq(personal_access_tokens::Column::Token, token)
                    .build(),
            )
            .one(db)
            .await?;
        token.ok_or_else(|| ModelError::EntityNotFound)
    }

    /// finds personal access tokens by the provided tokenable id and type
    ///
    /// # Errors
    ///
    /// When could not find personal access tokens or DB query error
    pub async fn find_by_tokenable(
        db: &DatabaseConnection,
        tokenable_type: &str,
        tokenable_id: &str,
    ) -> ModelResult<Vec<Self>> {
        let tokens = personal_access_tokens::Entity::find()
            .filter(
                model::query::condition()
                    .eq(
                        personal_access_tokens::Column::TokenableType,
                        tokenable_type,
                    )
                    .eq(personal_access_tokens::Column::TokenableId, tokenable_id)
                    .build(),
            )
            .all(db)
            .await?;
        Ok(tokens)
    }

    /// Asynchronously creates a personal access token and saves it to the database.
    ///
    /// # Errors
    ///
    /// When could not save the personal access token into the DB
    pub async fn create(db: &DatabaseConnection, params: &CreateParams) -> ModelResult<Self> {
        let token = personal_access_tokens::ActiveModel {
            tokenable_type: ActiveValue::set(params.tokenable_type.to_string()),
            tokenable_id: ActiveValue::set(params.tokenable_id.to_string()),
            name: ActiveValue::set(params.name.to_string()),
            token: ActiveValue::set(params.token.to_string()),
            abilities: ActiveValue::set(params.abilities.clone()),
            expires_at: ActiveValue::set(params.expires_at),
            ..Default::default()
        }
        .insert(db)
        .await?;

        Ok(token)
    }

    /// Deletes the personal access token
    ///
    /// # Errors
    ///
    /// When could not delete the personal access token from the DB
    pub async fn delete(self, db: &DatabaseConnection) -> ModelResult<DeleteResult> {
        let model: personal_access_tokens::ActiveModel = self.into();
        Ok(model.delete(db).await?)
    }
}

impl ActiveModel {
    /// Updates the personal access token's last used timestamp
    ///
    /// # Errors
    ///
    /// when has DB query error
    pub async fn update_last_used(mut self, db: &DatabaseConnection) -> ModelResult<Model> {
        self.last_used_at = ActiveValue::set(Some(chrono::Utc::now().into()));
        Ok(self.update(db).await?)
    }

    /// Updates the personal access token's expiration date
    ///
    /// # Errors
    ///
    /// when has DB query error
    pub async fn update_expires_at(
        mut self,
        db: &DatabaseConnection,
        expires_at: Option<DateTimeWithTimeZone>,
    ) -> ModelResult<Model> {
        self.expires_at = ActiveValue::set(expires_at);
        Ok(self.update(db).await?)
    }

    /// Updates the personal access token's abilities
    ///
    /// # Errors
    ///
    /// when has DB query error
    pub async fn update_abilities(
        mut self,
        db: &DatabaseConnection,
        abilities: Option<String>,
    ) -> ModelResult<Model> {
        self.abilities = ActiveValue::set(abilities);
        Ok(self.update(db).await?)
    }
}
